#include <oglapp.h>

#include <app\states\connectionState.h>
#include <app\states\cacheState.h>
#include <app\states\moveState.h>
#include <app\states\rotateState.h>

#include <base\quaternion.h>
#include <render\viewFrustum.h>

#define THIS oglapp::_instance

oglapp *oglapp::_instance = NULL;

typedef BOOL (APIENTRY *PFNWGLSWAPINTERVALFARPROC)( int );
PFNWGLSWAPINTERVALFARPROC wglSwapIntervalEXT = 0;

void setVSync(int interval=1)
{
  const char *extensions = (const char *) glGetString( GL_EXTENSIONS );

  if( strstr( extensions, "WGL_EXT_swap_control" ) == 0 ) {
    cerr <<  "Error: WGL_EXT_swap_control extension not supported on your computer." << endl;
    return;
  }
  else
  {
    wglSwapIntervalEXT = (PFNWGLSWAPINTERVALFARPROC)wglGetProcAddress( "wglSwapIntervalEXT" );

    if( wglSwapIntervalEXT )
      wglSwapIntervalEXT(interval);
  }
}

void 
oglapp::onKeyPress(unsigned char key, int x, int y)
{
  if (key>='A' && key<='Z') key+='a'-'A';

  switch (key) {
    case 27:
    case 'q':
    case 'x':
      THIS->view_resolver->stop();
      glutLeaveMainLoop();
      break;
    case 'f':
      glutFullScreen();
      break;
    case 'w':
      glutReshapeWindow(WINDOW_WIDTH, WINDOW_HEIGHT);
      glutPositionWindow(WINDOW_LEFT, WINDOW_TOP);
      break;
    default:
      break;
  }
}

void 
oglapp::onKeyDown(int key, int x, int y) {

  switch (key) {
    case GLUT_KEY_UP:    THIS->myavatar.key_up_pressed(true);    break;
    case GLUT_KEY_DOWN:  THIS->myavatar.key_down_pressed(true);  break;
    case GLUT_KEY_LEFT:  THIS->myavatar.key_left_pressed(true);  break;
    case GLUT_KEY_RIGHT: THIS->myavatar.key_right_pressed(true); break;
    default: break;
  }
}

void 
oglapp::onKeyUp(int key, int x, int y) {

  switch (key) {
    case GLUT_KEY_UP:    THIS->myavatar.key_up_pressed(false);    break;
    case GLUT_KEY_DOWN:  THIS->myavatar.key_down_pressed(false);  break;
    case GLUT_KEY_LEFT:  THIS->myavatar.key_left_pressed(false);  break;
    case GLUT_KEY_RIGHT: THIS->myavatar.key_right_pressed(false); break;
    default: break;
  }
}

void 
oglapp::onMouseButton(int button, int state, int x, int y) {
}

void
oglapp::onMousePassiveMotion(int x, int y) {

  vec2i diff(x - (THIS->window_size[0] / 2), y - (THIS->window_size[1] / 2));

  if (diff.x != 0 || diff.y != 0) {
    THIS->myavatar.rotate((float) diff[0], (float) diff[1]);
    glutWarpPointer((THIS->window_size[0] / 2), (THIS->window_size[1] / 2));
  }
}

void
oglapp::onMouseWheel(int wheel, int direction, int x, int y) {
  THIS->myavatar.changeAltitude((float) direction * 5.0f);
}

void 
oglapp::onResize(int w, int h) {
  glViewport(0, 0, w, h);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  THIS->mycamera.setAspectRatio((float) w / (float) h);
  gluPerspective(THIS->mycamera.getHeightAngle() * 180.0 / PI, THIS->mycamera.getAspectRatio(), THIS->mycamera.getNearDistance(), THIS->mycamera.getFarDistance());

  THIS->window_size = vec2i(w, h);

  viewingParametersState *vpstate = GET_STATE(viewingParametersState, streamingClient::instance().connection());
  vpstate->setWindowSize(THIS->window_size[0], THIS->window_size[1]);
  vpstate->info.frustum.setCamInternals(THIS->mycamera.getHeightAngle(), THIS->mycamera.getAspectRatio(), THIS->mycamera.getNearDistance(), THIS->mycamera.getFarDistance());
}

bool
oglapp::initializeOGLApplication() {

  THIS->window_size = vec2i(WINDOW_WIDTH, WINDOW_HEIGHT);

  // initialize cursor
  vec2i window_pos(glutGet(GLUT_WINDOW_X), glutGet(GLUT_WINDOW_Y));
  glutWarpPointer(window_pos[0] + THIS->window_size[0] / 2, window_pos[1] + THIS->window_size[1] / 2);

  // initialize application core
  THIS->mycamera.setNearDistance(0.1f);
  THIS->mycamera.setFarDistance(10000);
  THIS->mycamera.setHeightAngle((float) (60.0f * PI) / 180.0f);
  THIS->mycamera.setAspectRatio((float) WINDOW_WIDTH / (float) WINDOW_HEIGHT);

  // initialize time loop
  elapsed_time = 0.0;

  vpstate = GET_STATE(viewingParametersState, streamingClient::instance().connection());
  vpstate->setWindowSize(THIS->window_size[0], THIS->window_size[1]);
  vpstate->setLevelsInterval(0, 9);
  vpstate->info.frustum.setCamInternals(THIS->mycamera.getHeightAngle(), THIS->mycamera.getAspectRatio(), THIS->mycamera.getNearDistance(), THIS->mycamera.getFarDistance());

  moveState     *move_state = GET_STATE(moveState, streamingClient::instance().connection());
  rotateState *rotate_state = GET_STATE(rotateState, streamingClient::instance().connection());

  // propagate move and rotate changes of view volume to session state
  THIS->myavatar.addMoveObserver((moveObserver *) move_state);
  THIS->myavatar.addRotateObserver((rotateObserver *) rotate_state);

  THIS->myavatar.setStartLocation(49.9812545f, 14.230042f, 300.0, 19);

  glEnable(GL_TEXTURE_2D);

  // create view resovler, but start it later
  THIS->view_resolver = new viewResolver;
  vpstate->addViewChangeObserver((viewChangeObserver *) THIS->view_resolver);

  // propagate move and rotate change of view to view resolver
  move_state->addMoveObserver((moveObserver *) THIS->view_resolver);
  rotate_state->addRotateObserver((rotateObserver *) THIS->view_resolver);

  // initialize opengl
  glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
  glClearDepth(1.0f);
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glShadeModel(GL_FLAT);
  glPolygonMode(GL_FRONT, GL_FILL);
  glPolygonMode(GL_BACK, GL_FILL);
  //glPolygonMode(GL_FRONT, GL_LINE);
  //glPolygonMode(GL_BACK, GL_LINE);
  //glEnable(GL_CULL_FACE);

  // start the view resolver
  THIS->view_resolver->start();

  // some rendering stuff
  setVSync(0);
  frame_time = time = timebase = fps = frames = 0;

  return true;
}

void 
oglapp::drawTargetCross() {

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0, THIS->window_size[0], THIS->window_size[1], 0, -1, 1);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
        
  glColor3f(1.0, 1.0, 1.0);
  glBegin(GL_LINES);
    float half_w = THIS->window_size[0] / 2.0f;
    float half_h = THIS->window_size[1] / 2.0f;
    glVertex2f(half_w - 5.0f, half_h + 0.0f);
    glVertex2f(half_w + 5.0f, half_h + 0.0f);
    glVertex2f(half_w + 0.0f, half_h - 5.0f);
    glVertex2f(half_w + 0.0f, half_h + 5.0f);
  glEnd();

}

void
oglapp::resolveFPS() {

  if (THIS->time - THIS->timebase > 50) {
    THIS->timebase = THIS->time;
  }

  THIS->time = glutGet(GLUT_ELAPSED_TIME);
  if (THIS->time - THIS->frame_time > 100) {
    // recompute FPS
    double diff = (double)(THIS->time - THIS->frame_time);
    if (diff > 0.0) THIS->fps = ((1000 * THIS->frames) / (int) diff);
    THIS->frames = 0;
  }

  glColor3f(1.0, 0.0, 0.0);
  stringstream s;
  s << "FPS: " << THIS->fps << '\0';
  THIS->renderBitmapString(30, 35, s.str());

}

void
oglapp::renderBitmapString(float x, float y, string str) {
  
  void * font = GLUT_BITMAP_8_BY_13;

  // set position to start drawing fonts
  glRasterPos2f(x, y);
  // loop all the characters in the string
  for (int i = 0; i != str.length() - 1; i++) {
    glutBitmapCharacter(font, str[i]);
  }

}
void 
oglapp::onDisplay(void) {

  THIS->time=glutGet(GLUT_ELAPSED_TIME);  

  if (THIS->frames == 0) {
    THIS->frame_time = THIS->time;
  }

  THIS->frames++;

  // smooth movement of the avatar
	if (THIS->time - THIS->timebase > 50) {
    THIS->myavatar.setSpeed((float) (THIS->time - THIS->timebase));
    THIS->myavatar.translate();
	}

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(THIS->mycamera.getHeightAngle() * 180.0 / PI, THIS->mycamera.getAspectRatio(), THIS->mycamera.getNearDistance(), THIS->mycamera.getFarDistance());

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  glRotatef(90.0, 1.0, 0.0, 0.0);

  THIS->myavatar.applyRotation();
  THIS->myavatar.applyTranslation();

  glColor3f(0.2f, 1.0f, 0.2f);

  // render scene
  THIS->view_resolver->render();

  // render HUD target cross
  THIS->drawTargetCross();

  glColor3f(1.0, 0.0, 0.0);
  // resolve and print FPS
  THIS->resolveFPS();

  // change time base for moving
  //Sleep(10);

  //glFlush();
  glutSwapBuffers();
}

bool 
oglapp::start(int argc, char **argv) {
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowPosition(WINDOW_LEFT, WINDOW_TOP);
  glutInitWindowSize(WINDOW_WIDTH, WINDOW_HEIGHT);
  glutCreateWindow(WINDOW_TITLE);
  glutDisplayFunc(onDisplay);
  glutReshapeFunc(onResize);
  glutKeyboardFunc(onKeyPress);
  glutSpecialFunc(onKeyDown);
  glutSpecialUpFunc(onKeyUp);
  glutMouseFunc(onMouseButton);
  glutPassiveMotionFunc(onMousePassiveMotion);
  glutMouseWheelFunc(onMouseWheel);
  glutIdleFunc(onDisplay);

  glutSetCursor(GLUT_CURSOR_NONE);

  bool initialized = initializeOGLApplication();

  return initialized;
}

oglapp::oglapp() {

}

oglapp::~oglapp() {
  if (view_resolver) {
    view_resolver->stop();  
    delete view_resolver;
  }
}

oglapp &
oglapp::instance() {
  if (!_instance) {
    _instance = new oglapp;
  }

  return *_instance;
}
